package bfw3b9.atprin.Interface;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.GLUtils;
import android.opengl.Matrix;
import android.util.Log;

/**
 * Der Renderer Hier wird die eigentliche Arbeit verrichtet (Zeichnen, Objekte
 * Identifizieren)
 * 
 * Aktuell noch unausgereift und eine Bunte Mischung aus folgenden Tutorials:
 * http://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_06
 * http://www.learnopengles.com/android-lesson-one-getting-started/
 * http://www.learnopengles.com/android-lesson-two-ambient-and-diffuse-lighting/
 * http://www.learnopengles.com/android-lesson-four-introducing-basic-texturing/
 * http://www.learnopengles.com/android-lesson-seven-an-introduction-to-vertex-
 * buffer-objects-vbos/
 * http://en.wikibooks.org/wiki/OpenGL_Programming/Stencil_buffer
 * http://en.wikibooks.org/wiki/OpenGL_Programming/Object_selection
 * 
 * @author Thomas Rinsch
 * 
 */
public class InterfaceRenderer implements GLSurfaceView.Renderer
{
	private final static String LOGTAG = "InterfaceRenderer";

	private final int BYTESPERFLOAT = Float.SIZE / Byte.SIZE;

	// Kameraskalierung fuer die Orthogonale Kamera
	private final float ORTHOSCALE = 10;

	private int mGLWidth;

	private int mGLHeight;

	/*
	 * Eine alternative GLES20 Implementierung, die benoetigt wird um mit
	 * gepackten, serverseitigen Buffern zu arbeiten! Grund ist, dass die
	 * Funktion "glVertexAttribPointer" unter Android streng genommen falsch
	 * implementiert ist und einen direkten Buffer, anstatt eines Pointers als
	 * Input benoetigt
	 * 
	 * Nur fuer Android 2.2 beneotigt"!
	 */
	// private AndroidGL20 mGlEs20;

	// Alle zu zeichnenden Interface-Objekte
	private InterfaceItem[] mItems;

	private final Context mContext;

	// pointer zum GLES-Programm (Shader)
	private int mDisplayShaderProgram;
	private int mIdentShaderProgram;

	// Pointer zu den Vertices?
	private int mPositionHandle;

	// Pointer zur Identityvalue
	private int mIDentityHandle;

	// Pointer zu den Texturen?
	private int mTextureUniformHandle;

	// Pointer zu den Texturenkoordinaten
	private int mTextureCoordinateHandle;

	// pointer zur Model-View Matrix
	private int mMVMatrixHandle;

	// Aktuell ausgewaehltes Objekt Werte: -2= Nichts ausgewaehlt -1=Objekt am
	// Touchpunkt ermitteln >=0: ID des gewaehlten Objekts
	private int mCurrentObject;

	// enthaelt die Bindings fuer die zwei Framebuffer pos. 0 fuer den default
	// framebuffer, pos 1 fuer den Ident-FB
	private int[] mFramebufferHandles;

	/*
	 * Transformationsmatrizen, um Projektionen zur Anpassung an das
	 * Bildschirmverhaeltnis durchzufuehren
	 */
	private int mMVPMatrixHandle;
	private float[] mMVPMatrix = new float[16];
	private float[] mVMatrix = new float[16];
	private float[] mProjMatrix = new float[16];
	private float[] mModelMatrix = new float[16];

	private int mTouchX;

	private int mTouchY;

	/**
	 * Konstruktor
	 * 
	 * @param _context
	 * @param _items
	 */
	public InterfaceRenderer(final Context _context, InterfaceItem[] _items)
	{
		mContext = _context;
		mItems = _items;
		// mGlEs20 = new AndroidGL20(); // siehe Deklaration
	}

	/**
	 * Bildschirm neu zeichnen
	 */
	public void onDrawFrame(GL10 unused)
	{		
		Log.d( LOGTAG, "onDraw aufgerufen" );
		/*
		 * Hier werden zwei Renderpaesse durchgefuehrt. 
		 * Einmal fuer den Display-Render
		 * und einmal fuer das Rendern der Identity-Values
		 */
		for (int fBuff = 0; fBuff <= 1; fBuff++)
		{
			GLES20.glBindFramebuffer( GLES20.GL_FRAMEBUFFER, mFramebufferHandles[fBuff] );
			if (fBuff == 1)
			{
				Log.d( LOGTAG, "Zur Zeit ausgewaehltes Objekt ermitteln" );

				// Y muss invertiert werden, wegen der Art wie OpenGL das
				// Fenster liest
				ByteBuffer color = ByteBuffer.allocateDirect( 3 ).order( ByteOrder.nativeOrder() );

				// Auslesen des Pixels an den Touch-Koordinaten
				InterfaceController.ErrorCheck( GLES20.glGetError(), 0, LOGTAG, "Errorcode vorm Lesen des Framebuffers" );
				GLES20.glReadPixels( mTouchX, mGLHeight - mTouchY - 1, 1, 1, GLES20.GL_RGB, GLES20.GL_UNSIGNED_BYTE, color );
				InterfaceController.ErrorCheck( GLES20.glGetError(), 0, LOGTAG, "Errorcode nach dem Lesen des Framebuffers" );

				Log.d( LOGTAG, "ID-Werte an Pixel X: " + mTouchX + " Y: " + mTouchY );
				Log.d( LOGTAG,
						"Rot: " + String.valueOf( (int) color.get( 0 ) & 0xff ) + " Gruen: " + String.valueOf( (int) color.get( 1 ) & 0xff ) + " Blau: " + String.valueOf( (int) color.get( 2 ) & 0xff ) );

				mCurrentObject = ( (int) color.get( 0 ));
				if(mCurrentObject < 1)
				{
					Log.d( LOGTAG, "Kein Objekt ausgewaehlt");
				}else
				{
					Log.d( LOGTAG, "Zur Zeit ausgewaehltes Objekt: " + InterfaceItem.getItem( mItems, mCurrentObject ) );
				}
				
				color.limit( 0 );
				color = null;
			}

			// Buffer leeren
			// Hintergrundfarbe setzen (RGBA)
			GLES20.glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
			GLES20.glClear( GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT );

			// Handles / Pointer setzen
			if(fBuff != 1)
			{
				// Display-Shader anwenden, wenn wir auf fBuff 0 zeichnen
				GLES20.glUseProgram( mDisplayShaderProgram );
				// Dithering einschalten um das Bild angenehmer erscheinen zu lassen
				GLES20.glEnable( GLES20.GL_DITHER );
				mMVPMatrixHandle = GLES20.glGetUniformLocation( mDisplayShaderProgram, "u_MVPMatrix" );
				mPositionHandle = GLES20.glGetAttribLocation( mDisplayShaderProgram, "a_Position" );
				mTextureUniformHandle = GLES20.glGetUniformLocation( mDisplayShaderProgram, "u_Texture" );
				mTextureCoordinateHandle = GLES20.glGetAttribLocation( mDisplayShaderProgram, "a_TexCoordinate" );
			}else
			{
				// Ident-Shader anwenden, wenn wir auf fBuff 1 zeichnen
				GLES20.glUseProgram( mIdentShaderProgram );
				// Dithering abschalten, um die IDs nicht zu verfaelschen
				GLES20.glDisable( GLES20.GL_DITHER );
				mMVPMatrixHandle = GLES20.glGetUniformLocation( mIdentShaderProgram, "u_MVPMatrix" );
				mPositionHandle = GLES20.glGetAttribLocation( mIdentShaderProgram, "a_Position" );
				mIDentityHandle = GLES20.glGetUniformLocation( mIdentShaderProgram, "a_Identity" );
				mTextureCoordinateHandle = GLES20.glGetAttribLocation( mIdentShaderProgram, "a_TexCoordinate" );	// Keine Ahnung warum, aber es hat Auswirkungen auf die Objektgroesse beim Rendern!
			}
			
			

			if (mItems != null)
			{
				// Zeichne alle Items
				for (int i = 0; i < mItems.length; i++)
				{
					// Falls der Identity-Buffer geschrieben wird, uebergib die Item-ID
					if (fBuff == 1)
					{					
						float identValue = ( (float) mItems[i].getID() ) / 256.0f;	// Die Item-ID auf den Farbraum umrechnen
						GLES20.glUniform1f( mIDentityHandle, identValue );
						
						// Keine Textur laden
						GLES20.glBindTexture( GLES20.GL_TEXTURE_2D, 0 );
					} else
					{
						// Dieser unit die korrekte Textur zuweisen
						GLES20.glBindTexture( GLES20.GL_TEXTURE_2D, mItems[i].getTexture() );
					}

					InterfaceController.ErrorCheck( GLES20.glGetError(), 0, LOGTAG, "Fehler vor dem Zeichnen von Item + " + String.valueOf( i ) );
					drawMesh( mItems[i], i );
				}
			}
		}
		// Identity Buffer wieder leeren
		GLES20.glDisableVertexAttribArray( mIDentityHandle );

		// Den Displayframebuffer binden, damit dieser wieder angezeigt wird.
		GLES20.glBindFramebuffer( GLES20.GL_FRAMEBUFFER, mFramebufferHandles[1] );
		InterfaceController.ErrorCheck( GLES20.glGetError(), 0, LOGTAG, "Fehler am Ende von onDraw" );
		Log.d( LOGTAG, "onDraw erfolgreich" );
	}

	
	/**
	 * Wird bei der Aenderung der Canvas aufgerufen
	 * (Auch nach onSurfaceCreated)
	 */
	public void onSurfaceChanged(GL10 unused, int width, int height)
	{
		Log.d(LOGTAG, "Fenstergroesse hat sich geaendert");
		// Renderflaeche an die neuen Masse anpassen
		GLES20.glViewport( 0, 0, width, height );

		// Hoehe und Breite vom OpenGL Fenster speichern
		mGLWidth = width;
		mGLHeight = height;
				
		// Das Seitenverhaeltnis der Anzeige ausrechnen, wichtig fuer die
		// Viewport-Manipulation
		float ratio = (float) width / (float) height;

		// Die Projektionsmatrix berechnen
		// CLippingmatrix!!!! 
		Matrix.orthoM( mProjMatrix, 0, -ratio * ORTHOSCALE, ratio * ORTHOSCALE, -1 * ORTHOSCALE, 1 * ORTHOSCALE, 1, 20 );

		// Eine Kamera-Projektionsmatrix definieren
		// Rsultmatrix, offset ,AugeX, AugeY, AugeZ, CenterX, CenterY, CenterZ,
		// UpX, UpY, UpZ
		Matrix.setLookAtM( mVMatrix, 0, 0.0f, 0.0f, 1.5f, 0.0f, 0.0f, -5.0f, 0.0f, 1.0f, 0.0f );

		
	}

	/**
	 * Wird bei der Initialisierung aufgerufen
	 */
	public void onSurfaceCreated(GL10 unused, EGLConfig config)
	{
		Log.d( LOGTAG, "OnSurface Created aufgerufen" );
		// Hintergrundfarbe setzen (RGBA)
		GLES20.glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
		// Texturen aktivieren
		GLES20.glEnable( GLES20.GL_TEXTURE_2D );
		GLES20.glEnable( GLES20.GL_BLEND );
		GLES20.glBlendFunc( GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA );

		/*
		 * Am Anfang soll er die zu zeichnenden Objekte initialisieren Es sollte
		 * aus Performancegruenden unbedingt vermieden werden, Objekte in onDraw
		 * zu initialisieren!!!!
		 */

		// Shadercode erzeugen
		mDisplayShaderProgram = Shader.initShader( Shader.DISPLAY_VERTEX_SHADER_CODE, Shader.DISPLAY_FRAGMENT_SHADER_CODE );
		mIdentShaderProgram = Shader.initShader( Shader.IDENT_VERTEX_SHADER_CODE, Shader.IDENT_FRAGMENT_SHADER_CODE );

		// Alle Texturen laden
		loadAllTextures( mItems );
		// Die aktive Textureunit setzen
		GLES20.glActiveTexture( GLES20.GL_TEXTURE0 );
		InterfaceController.ErrorCheck( GLES20.glGetError(), 0, LOGTAG, "Fehler beim Initialisieren der Daten" );
		/*
		 * Buffer binden Koennte (wahrscheinlich) auch im Item selber gemacht
		 * werden. Allerdings muss es zwingend im gleichen Thread wie OpenGL
		 * laufen. Hiermit gehe ich auf nummer Sicher
		 */
		if (mItems != null)
		{
			Log.d( LOGTAG, "Modelldaten an OpenGL senden" );
			int buffer[] = new int[1];
			for (int i = 0; i < mItems.length; i++)
			{
				Log.d( LOGTAG, "Item " + i + " wird gesendet" );
				// Buffer binden
				GLES20.glGenBuffers( 1, buffer, 0 );
				GLES20.glBindBuffer( GLES20.GL_ARRAY_BUFFER, buffer[0] );
				// Daten an OpenGL uebertragen
				GLES20.glBufferData( GLES20.GL_ARRAY_BUFFER, mItems[i].getMeshData().capacity() * BYTESPERFLOAT, mItems[i].getMeshData(), GLES20.GL_STATIC_DRAW );
				// Bufferressourcen befreien (ist jetzt an GL ueberfuehrt)
				GLES20.glBindBuffer( GLES20.GL_ARRAY_BUFFER, 0 );
				// Die Bufferreferenz (den Pointer / Handle) im Item
				// festschreiben
				mItems[i].setBufferID( buffer[0] );

				Log.d( LOGTAG, "Item " + i + " gesendet" );
			}
		}

		// Hoehe und Breite vom OpenGL Fenster speichern
		IntBuffer buffer = IntBuffer.allocate( 4 );
		GLES20.glGetIntegerv( GLES20.GL_VIEWPORT, buffer );
		mGLWidth = buffer.get( 2 );
		mGLHeight = buffer.get( 3 );

		mCurrentObject = -2; // Aktuelles Objekt auf -2
		buffer.limit( 1 );
		buffer.clear();

		// zweiten Framebuffer erzeugen und ins Framebufferarray eintragen
		GLES20.glGenFramebuffers( 1, buffer );

		mFramebufferHandles = new int[2];
		mFramebufferHandles[0] = 0;
		mFramebufferHandles[1] = buffer.get( 0 );
		
		// Framebuffer fuers anhaengen der Renderbuffer aktivieren
		GLES20.glBindFramebuffer( GLES20.GL_FRAMEBUFFER, mFramebufferHandles[1] );

		// Color-Renderbuffer generieren und an den neuen Framebuffer anhaengen
		GLES20.glGenRenderbuffers( 1, buffer );
		GLES20.glBindRenderbuffer( GLES20.GL_RENDERBUFFER, buffer.get( 0 ) );
		GLES20.glRenderbufferStorage( GLES20.GL_RENDERBUFFER, GLES20.GL_RGB, mGLWidth, mGLHeight );

		GLES20.glFramebufferRenderbuffer( GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_RENDERBUFFER, buffer.get( 0 ) );

		// Pruefen, ob die Initialisierung erfolgreich war
		InterfaceController.ErrorCheck( GLES20.glCheckFramebufferStatus( GLES20.GL_FRAMEBUFFER ), 
				GLES20.GL_FRAMEBUFFER_COMPLETE, LOGTAG, "Fehler beim initialisieren des Framebuffers: " );

		buffer.limit( 0 );
		buffer.clear();

		Log.d( LOGTAG, "onSurfaceCreated erfolgreich" );
	}

	
	/**
	 * Zeichnet den gerade im Vertex-Buffer befindlichen Mesh
	 * 
	 * @param _item Das zu zeichnende Item
	 * @param _i Die ID des Items
	 */
	private void drawMesh(InterfaceItem _item, int _i)
	{
		// Log deaktiviert, da er die Debug-Konsole zuspammt
		//Log.d( LOGTAG, "Zeichne Objekt: " + _i );
		InterfaceController.ErrorCheck( GLES20.glGetError(), 0, LOGTAG, "Fehler vor dem Zeichnen von Item + " + String.valueOf( _i ) );
		// Transformationen ausfuehren
		// Die Model-Matrix (Also den Objectspace) vor jeder Aktion in den
		// ursprung des Worldspace verschieben
		Matrix.setIdentityM( mModelMatrix, 0 );
		// Danach von diesem Mittelpunkt aus die Verschiebungen durchfuehren
		Matrix.translateM( mModelMatrix, 0, _item.x, _item.y, 0.0f );

		// Modelldaten laden
		GLES20.glBindBuffer( GLES20.GL_ARRAY_BUFFER, _item.getBufferID() );
		GLES20.glEnableVertexAttribArray( mPositionHandle );
		GLES20.glVertexAttribPointer( mPositionHandle, _item.getVertexSize(), GLES20.GL_FLOAT, false, _item.getBlockSize(), 0 );

		// Texturkoordinaten laden
		GLES20.glBindBuffer( GLES20.GL_ARRAY_BUFFER, _item.getBufferID() );
		GLES20.glEnableVertexAttribArray( mTextureCoordinateHandle );
		GLES20.glVertexAttribPointer( mTextureCoordinateHandle, _item.getTexCoordSize(), GLES20.GL_FLOAT, false, _item.getBlockSize(), _item.getVertexSize() * BYTESPERFLOAT );

		// Den Buffer wieder leeren, so dass er in Zukunft nicht mehr genutzt
		// wird??
		GLES20.glBindBuffer( GLES20.GL_ARRAY_BUFFER, 0 );

		// Modelldaten vom Objectspace in den Worldspace ueberfuehren
		Matrix.multiplyMM( mMVPMatrix, 0, mVMatrix, 0, mModelMatrix, 0 );

		// Die ModelView-Matrix an OpenGL ueberfuehren
		GLES20.glUniformMatrix4fv( mMVMatrixHandle, 1, false, mMVPMatrix, 0 );

		// ModelView-Matrix mit Projektionsmatrix multiplizieren
		Matrix.multiplyMM( mMVPMatrix, 0, mProjMatrix, 0, mMVPMatrix, 0 );

		// MVP-Matrix an OpenGL ueberfuehren
		GLES20.glUniformMatrix4fv( mMVPMatrixHandle, 1, false, mMVPMatrix, 0 );

		// Objekt zeichnen
		GLES20.glDrawArrays( GLES20.GL_TRIANGLES, 0, _item.getVertexCount() );
		InterfaceController.ErrorCheck( GLES20.glGetError(), 0, LOGTAG, "Fehler nach Zeichnen von Item + " + String.valueOf( _i ) );
	}

	/**
	 * Gibt die ID des Items zurueck, auf das Geklickt wurde
	 * 
	 * @param _x
	 * @param _y
	 * @return Item-ID | 0 fuer kein Item
	 */
	public Byte prepareTouch(int _x, int _y)
	{
		mTouchX = _x;
		mTouchY = _y;

		mCurrentObject = -1;

		return 4;

	}

	/**
	 * Laedt die Items in den Renderer
	 * @param _items
	 */
	public void setItems(InterfaceItem[] _items)
	{
		mItems = _items;
	}

	/**
	 * Weist die Items an, ihre Texturen nach OpenGL zu ueberfuehren
	 * @param _items Itemliste
	 */
	private void loadAllTextures(final InterfaceItem[] _items)
	{
		for (int i = 0; i < mItems.length; i++)
		{
			_items[i].loadTextureData( mContext );
		}
	}

	/**
	 * Laedt eine Bilddatei aus dem "res" Ordner und portiert Sie nach OpenGL
	 * Quelle:
	 * http://www.learnopengles.com/android-lesson-four-introducing-basic
	 * -texturing/
	 * 
	 * @param context
	 * @param resourceId
	 * @return Pointer/Handle auf das OpenGL Objekt
	 */
	public static int loadTexture(final Context context, final int resourceId)
	{
		Log.d(LOGTAG, "Textur laden");
		// Erzeugen und verlinken der Handle fuer das OpenGL-Objekt
		final int[] textureHandle = new int[1];

		GLES20.glGenTextures( 1, textureHandle, 0 );

		// Die Eingabedatei in ein Android-Bitmap konvertieren
		final BitmapFactory.Options options = new BitmapFactory.Options();
		options.inScaled = false; /* Verhindert, dass das Bitmap bei
								   * verschiedenen Displaygroessen skaliert
								   * wird
								   */

		// Das Bitmap im Android-Format laden
		final Bitmap bitmap = BitmapFactory.decodeResource( context.getResources(), resourceId, options ); 

		// Textur binden und die Filteroptionen (Min und Max) setzen
		// (rudimentaeres Filtern)
		GLES20.glBindTexture( GLES20.GL_TEXTURE_2D, textureHandle[0] );

		GLES20.glTexParameteri( GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST );
		GLES20.glTexParameteri( GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST );

		// Die Textur wurde nur an OpenGL gebunden. Jetzt muss noch das Bitmap
		// in die Textur geladen werden
		GLUtils.texImage2D( GLES20.GL_TEXTURE_2D, 0, bitmap, 0 ); // Die Nullen:
																	// erste:
																	// Nur fuer
																	// Mip-Mapping(Die
																	// Texturlevel),
																	// zweite:
																	// Texturrand

		/*
		 * WICHTIG: Das Bitmap wird nicht in der Dalvik-VM, sondern Nativ
		 * gespeichert. Insofern wird es nur bedingt vom GarbageCollector
		 * entfernt. Da es aber nicht mehr benoetigt wird (OpenGL hat es ja
		 * jetzt in einem eigenen Format), muss es manuell geloescht werden.
		 * Sonst kann der Speicher zumuellen (bis hin zum Absturz. Bei
		 * OutOfMemory Error ausserhalb der VM gibt es aber nur bedingt
		 * Debugmeldungen!)
		 */
		bitmap.recycle();

		// Pruefen, ob das laden erfolgreich war
		if (textureHandle[0] == 0)
		{
			throw new RuntimeException( "Error loading texture." );
		}

		return textureHandle[0];
	}
	
	/**
	 * Gibt das Seitenverhaeltnis des Fensters zurueck
	 * @return
	 */
	public float getRatio()
	{
		return ((float)mGLWidth/(float)mGLHeight);
	}
	
	/**
	 * Gibt die Orthogonalskala zurueck
	 * @return
	 */
	public float getOrthoscale()
	{
		return ORTHOSCALE;
	}

}
